package com.pioneer.cloudstorage.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pioneer.cloudstorage.common.constant.MessageConstant;
import com.pioneer.cloudstorage.common.exception.BaseException;
import com.pioneer.cloudstorage.mapper.FilesMapper;
import com.pioneer.cloudstorage.pojo.dto.ChangeFolderDTO;
import com.pioneer.cloudstorage.pojo.dto.CreateFolderDTO;
import com.pioneer.cloudstorage.pojo.dto.FileUploadDTO;

import com.pioneer.cloudstorage.pojo.entity.FileContentIndex;
import com.pioneer.cloudstorage.pojo.entity.FileMetadata;
import com.pioneer.cloudstorage.pojo.entity.Result;
import com.pioneer.cloudstorage.pojo.vo.ResultUpload;
import com.pioneer.cloudstorage.service.FilesService;
import com.pioneer.cloudstorage.utils.BaseContext;
import com.pioneer.cloudstorage.utils.FileUtils;
import com.pioneer.cloudstorage.utils.MD5Util;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;

@Service
@Slf4j
public class FilesServiceImpl extends ServiceImpl<FilesMapper, FileMetadata> implements FilesService {
    @Autowired
    private FilesMapper fileMetadataMapper;
    @Override
public List<FileMetadata > list(Integer parentId) {
    Long userId = BaseContext.getCurrentId();
    return this.list(new LambdaQueryWrapper<FileMetadata >()
            .eq(FileMetadata ::getUserId, userId)
            .eq(FileMetadata ::getParentId, parentId)
            .eq(FileMetadata ::getStatus, 1));
}

    @Override
    public Result createFolder(CreateFolderDTO createFolderDTO) {
        Long userId = BaseContext.getCurrentId();
        //校验同级目录下同名冲突。
        FileMetadata  file1 = this.getOne(new LambdaQueryWrapper<FileMetadata >()
                .eq(FileMetadata ::getUserId, userId)
                .eq(FileMetadata ::getFileName, createFolderDTO.getFolderName())
                .eq(FileMetadata ::getParentId, createFolderDTO.getParentId())
                .eq(FileMetadata::getStatus, 1));
        if (file1 != null) {
            throw new BaseException(MessageConstant.SAME_NAME_FOLDER);
        }
        FileMetadata  file = FileMetadata.builder()
                .userId(userId)
                .fileName(createFolderDTO.getFolderName())
                .filePath("")
                .parentId(createFolderDTO.getParentId())
                .isDirectory(1)
                .fileSize(0L)
                .fileMd5("")
                .mimeType("")
                .status(1)
                .createdTime(LocalDateTime.now())
                .updatedTime(LocalDateTime.now())
                .build();
        this.save(file);
        return Result.success();
    }



    @Value("${file.upload.base-dir:/tmp/clouddisk}")
    private String baseUploadDir;

    @Value("${file.upload.max-size:104857600}") // 100MB
    private long maxFileSize;

    @PostConstruct
    public void init() {
        // 如果是相对路径，转换为绝对路径
        File uploadDir = new File(baseUploadDir);
        if (!uploadDir.isAbsolute()) {
            // 获取项目根目录（jar包所在目录的父目录，或开发环境的项目根目录）
            String workDir = System.getProperty("user.dir");
            // 如果工作目录是 target 目录，则使用其父目录
            if (workDir.endsWith("target")) {
                workDir = new File(workDir).getParent();
            }
            baseUploadDir = new File(workDir, baseUploadDir).getAbsolutePath();
        }
        // 确保目录存在
        File dir = new File(baseUploadDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        log.info("文件上传目录: {}", baseUploadDir);
    }


    @Override
    @Transactional
    public ResultUpload uploadFile(FileUploadDTO fileUploadDTO, Long userId) {
        MultipartFile multipartFile = fileUploadDTO.getFile();
        Long parentId = fileUploadDTO.getParentId() == null ? 0L : fileUploadDTO.getParentId();

        // 1. 基础校验
        //validateFile(multipartFile);
        String fileMd5 = fileUploadDTO.getFileMd5();

        if(fileUploadDTO.getFile()== null ) {
            // 4. 秒传逻辑：检查是否已存在相同文件

            FileMetadata existingFile = fileMetadataMapper.selectByUserIdAndMd5(userId, fileMd5);
            if (existingFile != null) {
                log.info("文件已存在，实现秒传，MD5: {}", fileMd5);
                try {
                    if (fileMetadataMapper.countSameNameFile(userId, fileUploadDTO.getFileName(), parentId) > 0) {
                        throw new BaseException(MessageConstant.SAME_NAME_FILE);
                    }
                    // 创建新的文件记录指向已存在的物理文件
                    FileMetadata fileMetadata = this.getOne(new LambdaQueryWrapper<FileMetadata>()
                            .eq(FileMetadata::getFileMd5, fileMd5));
                    FileMetadata fileMetadata1 =  createFileMetadata(userId, fileUploadDTO.getFileName(), existingFile.getFilePath(),
                            parentId, 0, fileMetadata.getFileSize(),
                            fileMd5, fileMetadata.getMimeType());
                    // 异步处理文档内容索引
                    if (isDocumentFile(fileMetadata1.getMimeType())) {
                        asyncIndexFileContent(fileMetadata1);
                    }
                    return ResultUpload.success();
                } catch (Exception e) {
                    // 秒传过程中发生异常，抛出秒传失败异常
                    log.error("秒传失败", e);
                    throw new BaseException("秒传失败");
                }
            }
        }else {
        // 2. 检查文件名是否重复
        String originalFileName = null;
        try {
            originalFileName = multipartFile.getOriginalFilename();
            if (originalFileName == null || originalFileName.isEmpty()) {
                throw new IllegalArgumentException("文件名不能为空");
            }
        } catch (NullPointerException e) {
            log.error("multipartFile 对象为 null，无法获取原始文件名", e);
            throw new BaseException("文件上传参数错误");
        }
            if (fileMetadataMapper.countSameNameFile(userId, originalFileName, parentId) > 0) {
                throw new BaseException(MessageConstant.SAME_NAME_FILE);
            }
           if(fileMd5 == null||fileMd5.isEmpty()) {
               fileMd5 = calculateFileMd5(multipartFile);
           }
        // 5. 生成存储路径 - 使用MD5作为文件名的一部分便于管理
        String storagePath = generateStoragePath(userId, fileMd5, originalFileName);
        log.info("生成的存储路径: {}", storagePath);

        File destFile = new File(storagePath);

        // 6. 确保目录存在
        ensureDirectoryExists(destFile.getParentFile());
        log.info("目标文件: {}", destFile.getAbsolutePath());
        try {
            log.info("开始保存文件元信息");
            // 7. 保存文件到磁盘
            log.info("开始保存文件: {}", originalFileName);
            multipartFile.transferTo(destFile.getAbsoluteFile());

            log.info("文件保存成功: {}", storagePath);

            // 8. 保存文件元数据到数据库
            FileMetadata fileMetadata = null;
            fileMetadata = createFileMetadata(userId, originalFileName, storagePath,
                    parentId, 0, multipartFile.getSize(),
                    fileMd5, multipartFile.getContentType());
            // 9. 异步处理文档内容索引
            if (isDocumentFile(multipartFile.getContentType())) {
                asyncIndexFileContent(fileMetadata);
            }

            return ResultUpload.success();
        } catch (IOException e) {
            log.error("文件保存失败", e);
            // 如果保存失败，删除可能已创建的文件
            if (destFile.exists()) {
                destFile.delete();
            }
            throw new BaseException("文件上传失败");
        }
        }
        return ResultUpload.error("文件不存在，不能秒传");
    }


    private void validateFile(MultipartFile file) {
        if (file.isEmpty()) {
            throw new RuntimeException("文件不能为空");
        }

        if (file.getSize() > maxFileSize) {
            throw new RuntimeException("文件大小超过限制");
        }

        // 安全检查：防止路径遍历攻击
        String fileName = file.getOriginalFilename();
        if (fileName != null && (fileName.contains("..") || fileName.contains("/") || fileName.contains("\\"))) {
            throw new RuntimeException("文件名不合法");
        }
    }

    @Override
    public String calculateFileMd5(MultipartFile file) {
        try {
            return MD5Util.getMD5(file.getInputStream());
        } catch (IOException e) {
            log.error("计算文件MD5失败", e);
            throw new RuntimeException("文件处理失败");
        }
    }

    private String generateStoragePath(Long userId, String fileMd5, String originalFileName) {
        String datePath = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String fileExtension = FileUtils.getFileExtension(originalFileName);

        // 使用MD5作为文件名，避免重复存储相同文件
        String fileName = fileMd5 + (fileExtension.isEmpty() ? "" : "." + fileExtension);

        return Paths.get(baseUploadDir, String.valueOf(userId), datePath, fileName).toString();
    }

    private void ensureDirectoryExists(File directory) {
        if (!directory.exists()) {
            if (!directory.mkdirs()) {
                throw new RuntimeException("创建存储目录失败: " + directory.getAbsolutePath());
            }
        }
        if (!directory.canWrite()) {
            log.error("目录不可写: {}", directory.getAbsolutePath());
            throw new RuntimeException("目录没有写权限: " + directory.getAbsolutePath());
        }
    }

    private FileMetadata createFileMetadata(Long userId, String fileName, String filePath,
                                            Long parentId, Integer isDirectory, Long fileSize,
                                            String fileMd5, String mimeType) {
        FileMetadata fileMetadata = new FileMetadata(userId, fileName, filePath, parentId,
                isDirectory, fileSize, fileMd5, mimeType);

        int result = fileMetadataMapper.insert(fileMetadata);
        if (result > 0) {
            log.info("文件元数据保存成功，ID: {}", fileMetadata.getId());
            return fileMetadata;
        } else {
            throw new RuntimeException("文件元数据保存失败");
        }
    }

    @Override
    public boolean isDocumentFile(String mimeType) {
        if (mimeType == null) return false;

        return mimeType.startsWith("text/") ||
                mimeType.equals("application/pdf") ||
                mimeType.equals("application/msword") ||
                mimeType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document") ||
                mimeType.equals("application/vnd.ms-excel") ||
                mimeType.equals("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    }

    @Autowired
    private FileIndexService fileIndexService;
    @Async
    @Override
    public void asyncIndexFileContent(FileMetadata fileMetadata) {
        try {
            // 这里实现Elasticsearch索引逻辑
            // 使用Apache Tika等工具解析文件内容
            log.info("开始索引文件内容，文件ID: {}", fileMetadata.getId());
            // 模拟索引过程
            //hread.sleep(1000);
            // 委托给专门的索引服务
            fileIndexService.asyncIndexFileContent(fileMetadata);
            log.info("文件内容索引完成，文件ID: {}", fileMetadata.getId());

        } catch (Exception e) {
            log.error("文件内容索引失败，文件ID: {}", fileMetadata.getId(), e);
        }
    }

  /*  @Override
    public void deleteFile(Long fileId, Long userId) {
        // ... 删除文件逻辑

        // 异步删除索引
        fileIndexService.asyncDeleteFileIndex(fileId);
    }

    // 在文件重命名时更新索引
    @Override
    public void renameFile(Long fileId, String newName, Long userId) {
        // ... 重命名逻辑

        // 异步更新索引
        fileIndexService.asyncUpdateFileName(fileId, newName);
    }*/


    @Override
    public FileMetadata getFileMetadata(Long fileId, Long userId) {
        FileMetadata fileMetadata = fileMetadataMapper.selectById(fileId);

        if (fileMetadata == null) {
            throw new RuntimeException("文件不存在");
        }

        // 检查权限：用户只能下载自己的文件
        if (!fileMetadata.getUserId().equals(userId)) {
            throw new RuntimeException("没有权限访问该文件");
        }

        // 检查文件状态
        if (fileMetadata.getStatus() != null && fileMetadata.getStatus() != 1) {
            throw new RuntimeException("文件不可用");
        }

        return fileMetadata;
    }

    @Override
    public void downloadFile(Long fileId, Long userId, HttpServletResponse response) throws IOException {
        // 1. 获取文件元数据并验证权限
        FileMetadata fileMetadata = getFileMetadata(fileId, userId);

        // 2. 获取物理文件
        File physicalFile = getPhysicalFile(fileMetadata);
        if (physicalFile == null || !physicalFile.exists()) {
            throw new RuntimeException("文件不存在或已被删除");
        }

        if (!physicalFile.canRead()) {
            throw new RuntimeException("文件无法读取");
        }

        // 3. 设置响应头
        setDownloadHeaders(response, fileMetadata, physicalFile);

        // 4. 写入文件流
        try (InputStream inputStream = new FileInputStream(physicalFile)) {
            IOUtils.copy(inputStream, response.getOutputStream());
            response.getOutputStream().flush();

            log.info("文件下载成功 - 文件ID: {}, 用户ID: {}, 文件名: {}, 大小: {} bytes",
                    fileId, userId, fileMetadata.getFileName(), physicalFile.length());
        } catch (IOException e) {
            log.error("文件下载失败 - 文件ID: {}, 用户ID: {}", fileId, userId, e);
            throw new RuntimeException("文件下载失败");
        }
    }


    /**
     * 设置下载响应头
     */
    private void setDownloadHeaders(HttpServletResponse response, FileMetadata fileMetadata, File physicalFile) throws IOException {
        String fileName = fileMetadata.getFileName();

        // 对文件名进行编码，解决中文乱码问题
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString())
                .replaceAll("\\+", "%20");

        // 设置内容类型
        String contentType = getContentType(fileMetadata, physicalFile);
        response.setContentType(contentType);

        // 设置下载头信息
        response.setHeader("Content-Disposition",
                "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);

        // 设置文件大小
        response.setHeader("Content-Length", String.valueOf(physicalFile.length()));

        // 缓存控制
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Expires", "0");

        log.debug("设置下载头 - 文件名: {}, 类型: {}, 大小: {}",
                fileName, contentType, physicalFile.length());
    }

    /**
     * 获取内容类型
     */
    private String getContentType(FileMetadata fileMetadata, File physicalFile) {
        // 优先使用元数据中的MIME类型
        if (StringUtils.hasText(fileMetadata.getMimeType())) {
            return fileMetadata.getMimeType();
        }

        // 如果元数据中没有，尝试从文件扩展名判断
        try {
            String contentType = Files.probeContentType(physicalFile.toPath());
            if (contentType != null) {
                return contentType;
            }
        } catch (IOException e) {
            log.warn("无法探测文件类型: {}", physicalFile.getAbsolutePath(), e);
        }

        // 默认使用二进制流
        return "application/octet-stream";
    }

    /**
     * 根据文件元数据获取物理文件
     * 安全限制：只允许访问上传目录下的文件，防止路径遍历攻击
     */
    private File getPhysicalFile(FileMetadata fileMetadata) {
        if (fileMetadata == null || fileMetadata.getFilePath() == null) {
            return null;
        }

        String filePath = fileMetadata.getFilePath();
        log.debug("解析下载文件路径: {}", filePath);

        try {
            // 获取基础上传目录的标准路径
            File baseDir = new File(baseUploadDir);
            String baseDirCanonicalPath = baseDir.getCanonicalPath();
            
            // 构建文件路径
            File targetFile;
            
            // 如果存储的是绝对路径，直接使用
            File absoluteFile = new File(filePath);
            if (absoluteFile.isAbsolute()) {
                targetFile = absoluteFile;
            } else {
                // 如果是相对路径，相对于 baseUploadDir 解析
                targetFile = new File(baseDir, filePath);
            }
            
            // 获取目标文件的标准路径（会自动处理 ../ 等路径遍历符号）
            String targetCanonicalPath = targetFile.getCanonicalPath();
            
            // 安全检查：确保目标文件在允许的目录下
            if (!targetCanonicalPath.startsWith(baseDirCanonicalPath)) {
                log.error("安全警告：尝试访问上传目录外的文件 - 请求路径: {}, 解析路径: {}, 允许目录: {}", 
                         filePath, targetCanonicalPath, baseDirCanonicalPath);
                return null;
            }
            
            // 检查文件是否存在
            if (!targetFile.exists()) {
                log.warn("文件不存在: {}", targetCanonicalPath);
                return null;
            }
            
            // 检查是否为文件（不是目录）
            if (!targetFile.isFile()) {
                log.warn("路径不是文件: {}", targetCanonicalPath);
                return null;
            }
            
            log.debug("找到文件: {}", targetCanonicalPath);
            return targetFile;
            
        } catch (IOException e) {
            log.error("解析文件路径时发生错误: {}", filePath, e);
            return null;
        }
    }


    @Override
    public Result updateFile(Long fileId, ChangeFolderDTO updateFileDTO, Long userId) {
        FileMetadata fileMetadata = fileMetadataMapper.selectById(fileId);
        //1. 校验同级重名。
        int count = fileMetadataMapper.countSameNameFile(userId, updateFileDTO.getNewName(), fileMetadata.getParentId());
        if (count > 0) {
            log.error("同级目录下已存在同名文件 - 文件ID: {},  文件名: {}", fileId, updateFileDTO.getNewName());
            return Result.error("同级目录下已存在同名文件");
        }
       // 2. 更新 `file_metadata.file_name`。
         this.update(new LambdaUpdateWrapper<FileMetadata>()
                .eq(FileMetadata::getId, fileId)
                .eq(FileMetadata::getUserId, userId)
                .set(FileMetadata::getFileName, updateFileDTO.getNewName()));
        return Result.success();
    }
    @Override
    public Result deleteFile(Long fileId, Long userId) {
        FileMetadata fileMetadata = fileMetadataMapper.selectById(fileId);
        if (fileMetadata == null) {
            log.error("文件不存在 - 文件ID: {}", fileId);
            return Result.error("文件不存在");
        }
        if (!fileMetadata.getUserId().equals(userId)) {
            log.error("用户无权限删除文件 - 用户ID: {}, 文件ID: {}", userId, fileId);
            return Result.error("用户无权限删除文件");
        }
        //判断是文档就删除索引
        if (isDocumentFile(fileMetadata.getMimeType())) {
            fileIndexService.asyncDeleteFileIndex(fileId);
        }
        this.update(new LambdaUpdateWrapper<FileMetadata>()
                .eq(FileMetadata::getId, fileId)
                .eq(FileMetadata::getUserId, userId)
                .set(FileMetadata::getStatus, 2));

            deleteSubFilesAndFolders(fileId, userId);
            return Result.success();
    }

    /**
     * 递归删除子文件和子目录
     */
    private void deleteSubFilesAndFolders(Long parentId, Long userId) {
    // 获取所有子文件和子目录
    List<FileMetadata> children = this.list(new LambdaQueryWrapper<FileMetadata>()
            .eq(FileMetadata::getParentId, parentId)
            .eq(FileMetadata::getUserId, userId));

    // 更新所有子文件/子目录的状态为已删除
    if (!children.isEmpty()) {
        for (FileMetadata child : children) {
            // 递归处理每个子节点
            deleteSubFilesAndFolders(child.getId(), userId);
            //判断是文档就删除索引
            if (isDocumentFile(child.getMimeType())) {
                fileIndexService.asyncDeleteFileIndex(child.getId());
            }
            // 更新当前节点状态
            this.update(new LambdaUpdateWrapper<FileMetadata>()
                    .eq(FileMetadata::getId, child.getId())
                    .eq(FileMetadata::getUserId, userId)
                    .set(FileMetadata::getStatus, 2));
        }
    }
}

}

